package x.ovo.wechat.bot.impl.http.request.message;

import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okio.BufferedSink;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.net.url.UrlBuilder;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.crypto.SecureUtil;
import org.dromara.hutool.json.JSONObject;
import org.dromara.hutool.json.JSONUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import x.ovo.wechat.bot.core.Constant;
import x.ovo.wechat.bot.core.Context;
import x.ovo.wechat.bot.core.exception.ApiExcption;
import x.ovo.wechat.bot.core.http.request.ApiRequest;
import x.ovo.wechat.bot.core.http.result.UploadResult;
import x.ovo.wechat.bot.core.util.FileTypeUtil;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Date;
import java.util.function.Function;

@Slf4j(topic = "MediaUpload")
public class MediaUploadRequest extends ApiRequest<UploadResult> {

    private final File file;
    private static final long CHUNK_SIZE = 1 << 19;
    private final long chunkCount;
    private final String mimeType;
    private final JSONObject object;

    public MediaUploadRequest(File file, String to) {
        this.file = file;
        this.filename = file.getName();
        this.chunkCount = (file.length() + CHUNK_SIZE - 1) >> 19;
        this.mimeType = FileUtil.getMimeType(file.toPath());
        this.object = JSONUtil.ofObj()
                .set("BaseRequest", this.baseRequest)
                .set("UploadType", 2)
                .set("ClientMediaId", System.currentTimeMillis())
                .set("DataLen", file.length())
                .set("TotalLen", file.length())
                .set("StartPos", 0)
                .set("MediaType", 4)
                .set("FromUserName", this.session.getUserName())
                .set("ToUserName", to)
                .set("FileMd5", SecureUtil.md5(file));
    }

    @Override
    public ApiRequest<UploadResult> build() {
        String s = UrlBuilder.of(this.session.getUrl())
                .addPath(Constant.Urls.UPLOAD_FILE)
                .addQuery("f", "json")
                .build();
        return this.setUrl(s);
    }

    @Override
    public UploadResult execute() {
        this.build();
        OkHttpClient client = (OkHttpClient) Context.INSTANCE.getHttpEngine().getRawEngine();
        try {
            for (long chunk = 0; chunk < chunkCount; chunk++) {
                // 当前分片的起始位置
                long startPos = chunk * CHUNK_SIZE;
                // 当前分片的长度（最后一片可能会小于CHUNK_SIZE）
                long chunkLength = Math.min(this.file.length() - startPos, CHUNK_SIZE);

                RequestBody requestBody = this.buildRequestBody(startPos, chunkLength);
                MultipartBody multipartBody = this.buildMultipartBody(chunk, requestBody);
                Request request = new Request.Builder().url(this.getUrl()).post(multipartBody).build();

                log.info("文件上传： [{}] {} / {}", this.filename, chunk + 1, this.chunkCount);
                try (Response response = client.newCall(request).execute()) {
                    // 如果响应失败，则抛出异常
                    if (!response.isSuccessful())
                        throw new ApiExcption("文件上传响应错误，HTTP状态码：" + response.code());

                    // 将响应转换为实体类
                    UploadResult result = this.stringHandler().apply(response.body().string());
                    log.debug(result.toString());
                    // 如果上传失败，则抛出异常
                    if (result.failure())
                        throw new ApiExcption("文件上传失败，服务器返回状态码：" + result.getBaseResponse().getRet());
                    // 如果上传成功且媒体ID不为空，则返回上传结果
                    if (StrUtil.isNotBlank(result.getMediaId())) {
                        log.info("文件上传成功： [{}]", this.filename);
                        return result;
                    }
                }
            }
        } catch (Exception e) {
            log.error("文件上传失败： [{}]", this.filename);
            throw new ApiExcption(StrUtil.format("文件上传失败: [{}]", this.filename), e);
        }
        return null;
    }

    private @NotNull MultipartBody buildMultipartBody(long chunk, RequestBody requestBody) {
        MultipartBody.Builder builder = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("id", "WU_FILE_0")
                .addFormDataPart("name", this.filename)
                .addFormDataPart("type", this.mimeType)
                .addFormDataPart("size", String.valueOf(this.file.length()))
                .addFormDataPart("mediatype", FileTypeUtil.getFileType(this.filename))
                .addFormDataPart("uploadmediarequest", JSONUtil.toJsonStr(this.object))
                .addFormDataPart("webwx_data_ticket", this.session.getDataTicket())
                .addFormDataPart("pass_ticket", this.session.getPassTicket())
                .addFormDataPart("lastModifiedDate", new Date().toString())
                .addFormDataPart("filename", this.filename, requestBody);
        if (this.chunkCount > 1)
            builder.addFormDataPart("chunks", String.valueOf(this.chunkCount))
                    .addFormDataPart("chunk", String.valueOf(chunk));
        return builder.build();
    }

    private RequestBody buildRequestBody(long startPos, long chunkLength) {
        return new RequestBody() {
            @Override
            public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException {
                try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
                    raf.seek(startPos);
                    byte[] buffer = new byte[8192];
                    long remaining = chunkLength;
                    while (remaining > 0) {
                        int read = raf.read(buffer, 0, (int) Math.min(buffer.length, remaining));
                        if (read == -1) break;
                        bufferedSink.write(buffer, 0, read);
                        remaining -= read;
                    }
                }
            }

            @Nullable
            @Override
            public MediaType contentType() {
                return MediaType.parse(mimeType);
            }

            @Override
            public long contentLength() throws IOException {
                return chunkLength;
            }
        };
    }

    @Override
    protected Function<String, UploadResult> stringHandler() {
        return s -> JSONUtil.parseObj(s).toBean(UploadResult.class);
    }
}
